"""
OptionKit is a wrapper for Python's optparse module that forces the script 
writer to provide documentation. It works by parsing the script's docstring for 
the the args and options for the script.

For example, we write a module level docstring:

'''usage: %prog files [options]
   -d, --delete: delete all files
   -e, --erase=ERASE: erase the given file
   -F, --fill_pattern=0xFF: the fill pattern used to erase the given file'''

Then write a main program of this kind:

# sketch of a script to delete files
if __name__=='__main__':
    import OptionKit
    option,args=optionparse.parse(__doc__)
    if not args and not option: 
        OptionKit.abort()
    elif option.delete: 
        print "Delete all files"
    elif option.erase:
        print "Delete the given file with %s" % option.fill_pattern


Notice that ``OptionKit`` parses the docstring by looking at the
characters ",", ":", "=", "\\n", so be careful in using them. If
the docstring is not correctly formatted you will get a SyntaxError
or worse, the script will not work as expected.

* Place each option on a separate line.
* You can automatically assign default values by changing the value for the long
description. Instead of '--erase=ERASE' use '--erase=somevalue' (When the value 
is a case-insensitive match to the long parameter, it is ignored)

For an example, please see OptionKitExample.py in test.core.util
"""
import optparse, re, sys, os

# regular expression used to extract option string from docstring
USAGE = re.compile(r'(?s)\s*usage: (.*?)(\n[ \t]*\n|$)')

# will become the nonzero method of optparse.Values
def nonzero(self): 
    "True if options were given"
    for value in self.__dict__.itervalues():
        if value is not None: 
            return True
    return False

# dynamically fix optparse.Values
optparse.Values.__nonzero__ = nonzero 

class ParsingError(Exception):
    """Error thrown if parsing fails.""" 
    pass

# used by abort method
OPTION_STRING = ""

def abort(msg=""):
    """
    Called to abort option parsing. Prints msg or help.
    
    Keyword arguments:
    msg -- message to print on abort (default: help)
    """
    progname = os.path.basename(sys.argv[0])
    raise SystemExit(msg or OPTION_STRING.replace("%prog", progname))

def restrict_option(option, choices):    
    """
    Restrict the specified option to the list of choices

    Keyword arguments:
    option -- optparse option to restrict
    choices -- list of allowed strings
    """
    option.type = "choice"
    option.choices = choices

def make_prompt(name, default=None):    
    """
    create an interactive command line prompt for the specified option.

    Keyword arguments:
    name -- of variable to use in prompt
    default -- default value returned if user enters nothing (default None)
    """

    seq = ['please enter', name]

    if default is not None:
        default_string = ''.join(['{', default, '}'])
        seq.append(default_string)        

    seq.append(':')

    return ' '.join(seq)    

def prompt_user_input(option):
    """
    prompts the user for input for the specified optparse Option instance

    Keyword arguments:
    option -- optparse Option instance
    """
    prompt = make_prompt(option.dest, option.default)

    user_input = raw_input(prompt)

    if user_input == '' and option.default is not None:
        user_input = option.default

    if option.choices is not None:
        if user_input not in option.choices:
            print 'supplied option is not supported:', user_input
            print 'please choose among:', option.choices
            prompt_user_input(option)

    return user_input

def prompt_option_list(option_list, values, arg_dests=None):
    """
    Takes a list of OptionParser options and queries the user for their values.

    Keyword arguments:
    option_list -- list of optparse Option instances
    values -- optparse Values instance
    arg_dests -- list of string names for arguments 
    """
    for option in option_list:
        values.__dict__[option.dest] = prompt_user_input(option)

    arg_vals = []
    for arg_dest in arg_dests:
        user_input = raw_input(make_prompt(arg_dest))
        arg_vals.append(user_input)

    return arg_vals

def inspect_options(options):
    """
    Utility method to find which command line options were set by user. Returns
    a dictionary of {option : setting}
            
    Keyword arguments:
    values -- optparse.Values object returned by OptionParser
    """
    classattrs = dir(optparse.Values)
    settings = {}
    for name in [_ for _ in dir(options) if _ not in classattrs]:
        settings[name] = getattr(options, name)
        
    return settings
    

def parse(docstring):
    """
    Parse the arguments using the docstring as the template.
    
    Keyword arguments:
    docstring -- module docstring that contains command line template
    """

    global OPTION_STRING
    OPTION_STRING = docstring

    match = USAGE.search(OPTION_STRING)

    if not match:
        # documentation is insufficient 
        raise ParsingError("Cannot find the option string. \
            Please check your script's docstring")

    optlines = match.group(1).splitlines()

    try:
        parser = optparse.OptionParser(optlines[0])
        for line in optlines[1:]:
            optstr, helpstr = line.split(':')[:2]
            shortstr, longstr = optstr.split(',')[:2]        
            if '=' in optstr:
                # unless the value on the other side of = is the same
                # (modulo case) it is used as the default
                action = 'store'
                longstr, default = longstr.split('=')[:2]

                if default.lower() == longstr[3:]: # need to strip leading '--'
                    default = None
            else:
                action = 'store_true'

            # ensure that 'default' is set to None if it hasn't yet been defined
            if not vars().has_key('default'):
                default = None

            parser.add_option(shortstr.strip(), longstr.strip(), action=action, 
                              help=helpstr.strip(), default=default)
    except (IndexError, ValueError):
        raise ParsingError("Cannot parse the option string correctly")

    return parser